package com.itaming.lycheeframework.api.exception;

import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.itaming.lycheeframework.api.result.ApiResult;
import com.itaming.lycheeframework.api.result.CommonErrorCode;
import com.itaming.lycheeframework.support.utils.StringPool;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.validator.internal.engine.path.NodeImpl;
import org.hibernate.validator.internal.engine.path.PathImpl;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.validation.method.ParameterValidationResult;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.*;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.resource.NoResourceFoundException;

import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * 全局异常处理器
 *
 * @author A.Ming
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 异常解析器列表
     */
    private final List<ExceptionResolver> exceptionResolvers;

    /**
     * 构造函数
     *
     * @param exceptionResolvers 异常解析器列表
     */
    public GlobalExceptionHandler(List<ExceptionResolver> exceptionResolvers) {
        this.exceptionResolvers = exceptionResolvers != null ? exceptionResolvers : Collections.emptyList();
    }

    /**
     * 业务异常(400)
     *
     * @param e 异常
     * @return 统一结果
     */
    @ExceptionHandler(BusinessException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResult<?> handleException(BusinessException e) {
        printLog(e, String.format("(%s)%s", e.getCode(), e.getMessage()));
        return ApiResult.fail(e.getCode(), e.getMessage());
    }

    /**
     * 资源不存在异常(404)
     *
     * <p>
     * 适用于文件下载接口的文件不存在场景
     *
     * @param e 异常
     * @return 统一结果
     */
    @ExceptionHandler(NotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiResult<?> handleException(NotFoundException e) {
        printLog(e, String.format("(%s)%s", e.getCode(), e.getMessage()));
        return ApiResult.fail(e.getCode(), e.getMessage());
    }

    /**
     * 请求参数缺失(400)
     *
     * <p>
     * 适用于以下列方式接收的参数：
     * <ul>
     *   <li>{@code @CookieValue}，实际异常为：{@link MissingRequestCookieException}</li>
     *   <li>{@code @MatrixVariable}，实际异常为：{@link MissingMatrixVariableException}</li>
     *   <li>{@code @PathVariable}，实际异常为：{@link MissingPathVariableException}</li>
     *   <li>{@code @RequestHeader}，实际异常为：{@link MissingRequestHeaderException}</li>
     *   <li>{@code @RequestParam}，实际异常为：{@link MissingServletRequestParameterException}</li>
     * </ul>
     *
     * @param e 异常
     * @return 统一结果
     */
    @ExceptionHandler(MissingRequestValueException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResult<?> handleException(MissingRequestValueException e) {
        String message = String.format("%s: %s", CommonErrorCode.MISSING_PARAMETER.getMessage(), e.getMessage());
        printLog(e, message);
        return ApiResult.fail(CommonErrorCode.MISSING_PARAMETER, message);
    }

    /**
     * 请求参数类型不匹配(400)
     *
     * <p>
     * 参数类型转换错误，传入的参数类型与接收参数的类型不一致，如：传入的参数是字符串，而接收参数是数字。
     *
     * <p>
     * 适用于以下列方式接收的参数：
     * <ul>
     *   <li>{@code @CookieValue}</li>
     *   <li>{@code @MatrixVariable}</li>
     *   <li>{@code @PathVariable}</li>
     *   <li>{@code @RequestHeader}</li>
     *   <li>{@code @RequestParam}</li>
     * </ul>
     *
     * @param e 异常
     * @return 统一结果
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResult<?> handleException(MethodArgumentTypeMismatchException e) {
        String message = String.format("%s: %s", CommonErrorCode.PARAMETER_TYPE_MISMATCH.getMessage(), e.getName());
        printLog(e, message);
        return ApiResult.fail(CommonErrorCode.PARAMETER_TYPE_MISMATCH, message);
    }

    /**
     * 请求参数类型不匹配(400)
     *
     * <p>
     * 通常发生在 Spring 尝试转换参数，但没有找到合适的转换器时，会抛出此异常
     *
     * <p>
     * 如：当参数类型是自定义的对象类型(JavaBean)，且使用了 {@code @RequestParam} 等注解时，
     * 传入的参数会被直接转换为该对象类型，而参数都是字符串，所以无法直接转换为对象，就会抛出此异常。
     * <p>
     * 实际上，当参数类型是自定义的对象类型时，不应再使用 {@code @RequestParam} 注解，
     * 默认情况下，只要传入的参数名和接收参数的对象属性名一致，会自动绑定到对象的属性上。
     *
     * @param e 异常
     * @return 统一结果
     */
    @ExceptionHandler(MethodArgumentConversionNotSupportedException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResult<?> handleException(MethodArgumentConversionNotSupportedException e) {
        String message = String.format("%s: %s", CommonErrorCode.PARAMETER_TYPE_MISMATCH.getMessage(), e.getName());
        printLog(e, message);
        return ApiResult.fail(CommonErrorCode.PARAMETER_TYPE_MISMATCH, message);
    }

    /**
     * 请求参数类型不匹配(400)
     *
     * <p>
     * 适用于 {@code @RequestBody} 方式接收的参数
     * <p>
     * 原因：
     * <ul>
     *   <li>参数类型转换异常(如：字符串"a"转为Integer)</li>
     *   <li>参数为空</li>
     *   <li>参数属性类型转换错误</li>
     *   <li>参数json格式错误</li>
     * </ul>
     *
     * @param e 异常
     * @return 统一结果
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResult<?> handleException(HttpMessageNotReadableException e) {
        String message = null;

        Throwable cause = e.getCause();
        if (cause instanceof InvalidFormatException) {
            List<JsonMappingException.Reference> references = ((InvalidFormatException) cause).getPath();
            if (!references.isEmpty()) {
                String fieldName = references.stream()
                    .map(JsonMappingException.Reference::getFieldName)
                    .collect(Collectors.joining(StringPool.COMMA));
                message = String.format("%s: %s", CommonErrorCode.PARAMETER_TYPE_MISMATCH.getMessage(), fieldName);
            }
        }

        printLog(e, message);
        return ApiResult.fail(CommonErrorCode.PARAMETER_TYPE_MISMATCH, message);
    }

    /**
     * 参数校验异常(400)
     *
     * <p>
     * 适用于常规(非JavaBean)参数校验，且控制器类上未使用 {@code @Validated} 注解
     *
     * @param e 异常
     * @return 统一结果
     */
    @ExceptionHandler(HandlerMethodValidationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResult<?> handleException(HandlerMethodValidationException e) {
        String fieldName = e.getValueResults().stream()
            .map(ParameterValidationResult::getMethodParameter)
            .map(MethodParameter::getParameterName)
            .collect(Collectors.joining(StringPool.COMMA));

        String validationMessage = Optional.of(e.getDetailMessageArguments())
            .map(args -> args[0])
            .map(Object::toString)
            .orElse(null);

        String message = String.format("%s: [%s]%s", CommonErrorCode.PARAMETER_VALIDATION_FAILED.getMessage(), fieldName, validationMessage);

        printLog(e, message);
        return ApiResult.fail(CommonErrorCode.PARAMETER_VALIDATION_FAILED, message);
    }

    /**
     * 参数校验异常(400)
     *
     * <p>
     * 适用于常规(非JavaBean)参数校验，且控制器类使用了 {@code @Validated} 注解
     *
     * @param e 异常
     * @return 统一结果
     */
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResult<?> handleException(ConstraintViolationException e) {
        String message = null;

        ConstraintViolation<?> cv = Optional.ofNullable(e.getConstraintViolations())
            .flatMap(set -> set.stream().findFirst())
            .orElse(null);
        if (cv != null) {
            String fieldName = Optional.ofNullable(e.getConstraintViolations())
                .flatMap(set -> set.stream().findFirst())
                .map(ConstraintViolation::getPropertyPath)
                .map(v -> (PathImpl) v)
                .map(PathImpl::getLeafNode)
                .map(NodeImpl::getName)
                .orElse(null);

            if (fieldName != null) {
                message = String.format("%s: [%s]%s", CommonErrorCode.PARAMETER_VALIDATION_FAILED.getMessage(), fieldName, cv.getMessage());
            }
        }

        printLog(e, message);
        return ApiResult.fail(CommonErrorCode.PARAMETER_VALIDATION_FAILED, message);
    }


    /**
     * 参数校验异常(400)
     *
     * <p>
     * 适用于自定义对象(JavaBean)参数校验
     *
     * @param e 异常
     * @return 统一结果
     */
    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResult<?> handleException(BindException e) {
        String message = null;

        FieldError fieldError = e.getFieldError();
        if (fieldError != null) {
            String validationMessage = fieldError.isBindingFailure() ? StringPool.EMPTY : fieldError.getDefaultMessage();
            message = String.format("%s: [%s]%s", CommonErrorCode.PARAMETER_VALIDATION_FAILED.getMessage(), fieldError.getField(), validationMessage);
        }

        printLog(e, message);
        return ApiResult.fail(CommonErrorCode.PARAMETER_VALIDATION_FAILED, message);
    }

    /**
     * 不存在的请求(404)
     *
     * <p>
     * 需要以下配置：
     * <ul>
     *   <li>spring.mvc.throw-exception-if-no-handler-found=true (此配置项在 SpringBoot 3.x 中已被移除)</li>
     *   <li>spring.web.resources.add-mappings=false</li>
     * </ul>
     *
     * @param e 异常
     * @return 统一结果
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiResult<?> handleException(NoHandlerFoundException e) {
        String message = String.format("%s: %s", CommonErrorCode.NOT_FOUND.getMessage(), e.getRequestURL());
        printLog(e, message);
        return ApiResult.fail(CommonErrorCode.NOT_FOUND, message);
    }

    /**
     * 请求资源不存在(404)
     *
     * @param e 异常
     * @return 统一结果
     */
    @ExceptionHandler(NoResourceFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiResult<?> handleException(NoResourceFoundException e) {
        String message = String.format("%s: %s", CommonErrorCode.NOT_FOUND.getMessage(), e.getResourcePath());
        printLog(e, message);
        return ApiResult.fail(CommonErrorCode.NOT_FOUND, message);
    }

    /**
     * 不支持的请求方法(405)
     *
     * @param e 异常
     * @return 统一结果
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    public ApiResult<?> handleException(HttpRequestMethodNotSupportedException e) {
        String message = String.format("%s: %s", CommonErrorCode.METHOD_NOT_ALLOWED.getMessage(), e.getMethod());
        printLog(e, message);
        return ApiResult.fail(CommonErrorCode.METHOD_NOT_ALLOWED, message);
    }

    /**
     * 不支持的媒体类型(415)
     *
     * @param e 异常
     * @return 统一结果
     */
    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
    public ApiResult<?> handleError(HttpMediaTypeNotSupportedException e) {
        String message = String.format("%s: %s", CommonErrorCode.UNSUPPORTED_MEDIA_TYPE.getMessage(), e.getContentType());
        printLog(e, message);
        return ApiResult.fail(CommonErrorCode.UNSUPPORTED_MEDIA_TYPE, message);
    }

    /**
     * 通用异常处理
     *
     * <p>
     * 支持其他模块或外部项目通过异常解析器 {@link ExceptionResolver} 扩展
     *
     * <p>
     * 使用 {@code ResponseEntity} 返回，是为了动态返回http状态码，
     * 因为存在异常解析器扩展机制，使用 {@code @ResponseStatus} 无法满足功能需要。
     *
     * <p>
     * 此方法作为兜底逻辑，使用 {@code Exception} 接收参数，而不是 {@code Throwable}。
     * 因为 Spring 的 {@link DispatcherServlet} 的 {@code doDispatch} 方法
     * 会将 {@code Exception} 以外的 {@code Throwable}
     * 统一包装为 {@code ServletException}，并在后续逻辑中交给异常处理器处理。
     *
     * @param e 异常
     * @return 统一结果
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handleException(Exception e) {
        // 默认 http 状态
        HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
        // 默认错误码
        CommonErrorCode errorCode = CommonErrorCode.INTERNAL_SERVER_ERROR;

        // 解析异常
        ExceptionDescriptor exceptionDescriptor = resolveException(e);
        if (exceptionDescriptor != null) {
            // 默认 http 状态
            if (exceptionDescriptor.getHttpStatus() == null) {
                exceptionDescriptor.setHttpStatus(httpStatus);
            }

            // 默认状态码
            if (exceptionDescriptor.getCode() == null) {
                exceptionDescriptor.setCode(errorCode.getCode());
            }

            // 默认消息
            if (exceptionDescriptor.getMessage() == null) {
                exceptionDescriptor.setMessage(errorCode.getMessage());
            }

            // 处理异常
            printLog(e, String.format("(%s)%s", exceptionDescriptor.getCode(), exceptionDescriptor.getMessage()));
            return ResponseEntity
                .status(exceptionDescriptor.getHttpStatus())
                .body(ApiResult.fail(exceptionDescriptor.getCode(), exceptionDescriptor.getMessage()));
        }

        // 通用处理
        printLog(e, e.getMessage(), true);
        return ResponseEntity.status(httpStatus).body(ApiResult.fail(errorCode));
    }

    /**
     * 解析异常
     *
     * @param e 异常
     * @return 异常描述
     */
    private ExceptionDescriptor resolveException(Exception e) {
        for (ExceptionResolver resolver : exceptionResolvers) {
            if (resolver.supports(e.getClass())) {
                return resolver.resolve(e);
            }
        }

        return null;
    }

    /**
     * 打印日志
     *
     * @param e       异常
     * @param message 消息
     */
    public void printLog(Exception e, String message) {
        printLog(e, message, false);
    }

    /**
     * 打印日志
     *
     * @param e               异常
     * @param message         消息
     * @param printStackTrace 是否打印堆栈信息
     */
    public void printLog(Exception e, String message, boolean printStackTrace) {
        String msg = String.format("全局异常处理: %s >>> %s", e.getClass().getSimpleName(), message == null ? e.getMessage() : message);
        log.warn(msg, printStackTrace ? e : null);
    }

}
